﻿using Devil;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

namespace GameToolkit.BehaviourTree
{
    public abstract class BehaviourTreeExecutableMono : MonoBehaviour, IBehaviourTreeExecutable, IValidator
    {
        public event System.Action<string, EBlackboardAction> OnSetBlackboard;

        [SerializeField]
        BehaviourTreeAsset m_Asset;

        BehaviourTreePlayer mPlayer;
        readonly object mLock = new object();
        Dictionary<string, object> mBlackboard = new Dictionary<string, object>();
        ECullingUpdateMode mLastUpdate;
        public ECullingUpdateMode UpdateMode => mLastUpdate;

        public bool IsValid => this != null;

        string mCachedName;
        string IComponent.name
        {
            get
            {
                if (ParallelUtils.IsMainThread && this != null)
                    mCachedName = gameObject.name;
                return mCachedName;
            }
        }

#if UNITY_EDITOR
        string profiler;
#endif
        public BehaviourTreeAsset behaviourTreeAsset
        {
            get { return m_Asset; }
            set
            {
                if(m_Asset != value)
                {
                    m_Asset = value;
                    if (mPlayer != null)
                        mPlayer.SetBehaviourTree(value);
                }
            }
        }

        T IComponent.GetComponent<T>()
        {
            return GetComponentFromCache<T>();
        }

        protected virtual T GetComponentFromCache<T>()
        {
            if (ParallelUnity.IsMainThread)
                return this.GetComponent<T>();
            else
                return default;
        }

        public BehaviourTreePlayer GetPlayerForAsset(BehaviourTreeAsset asset)
        {
            return mPlayer == null ? null : mPlayer.FindPlayerForAsset(asset);
        }

        protected virtual void Awake()
        {
            mCachedName = name;
#if UNITY_EDITOR
            if (string.IsNullOrEmpty(profiler))
            {
                if (m_Asset == null)
                    profiler = $"BT({mCachedName}): <none>";
                else
                    profiler = $"BT({mCachedName}): {m_Asset.name}";
            }
#endif
        }

        protected virtual void OnDestroy()
        {
            StopBehaviour();
        }

        protected void StartBehaviour()
        {
            if (mPlayer == null)
            {
#if UNITY_EDITOR
                if (ParallelUtils.IsMainThread)
                {
                    mCachedName = name;
                    if (m_Asset == null)
                        profiler = $"BT({mCachedName}): <none>";
                    else
                        profiler = $"BT({mCachedName}): {m_Asset.name}";
                }
#endif
                mPlayer = new BehaviourTreePlayer(this);
                mPlayer.SetBehaviourTree(m_Asset);
            }
        }

        protected void UpdateBehaviour(float deltaTime, ECullingUpdateMode mode)
        {
            var player = mPlayer;
            if (player != null)
            {
#if UNITY_EDITOR
                Profiler.BeginSample(profiler, this);
#endif
                mLastUpdate = mode;
                player.UpdateBehaviour(deltaTime);
                mLastUpdate = 0;

#if UNITY_EDITOR
                Profiler.EndSample();
#endif
            }
        }

        protected void StopBehaviour()
        {
            var player = mPlayer;
            mPlayer = null;
            if (player != null)
            {
                player.Dispose();
            }
        }

        #region blackboard
        bool IsValidVarName(string varName)
        {
            if(string.IsNullOrEmpty(varName))
            {
#if UNITY_EDITOR
                var str = ParallelDispatcher.IsMainThread ? name : "BehaviourTreeRunner";
                Debug.LogWarning($"({str}) Invalid blackboard variable name (is empty).", this);
#endif
                return false;
            }
            return true;
        }

        bool IBehaviourTreeExecutable.IsBlackboardSet(string varName)
        {
            if (IsValidVarName(varName))
            {
                lock (mLock)
                {
                    return mBlackboard.ContainsKey(varName);
                }
            }
            else
            {
                return false;
            }
        }

        void IBehaviourTreeExecutable.SetBlackboard<T>(string varName, T value)
        {
            if (IsValidVarName(varName))
            {
                lock (mLock)
                {
                    mBlackboard[varName] = value;
                }
                OnSetBlackboard?.Invoke(varName, EBlackboardAction.Reset);
            }
        }

        void IBehaviourTreeExecutable.UnsetBlackboard(string varName)
        {
            if (IsValidVarName(varName))
            {
                lock (mLock)
                {
                    if (mBlackboard.ContainsKey(varName))
                        mBlackboard.Remove(varName);
                }
                OnSetBlackboard?.Invoke(varName, EBlackboardAction.Unset);
            }
        }

        bool IBehaviourTreeExecutable.GetBlackboard<T>(string varName, out T value)
        {
            if (IsValidVarName(varName))
            {
                lock (mLock)
                {
                    object v;
                    if (mBlackboard.TryGetValue(varName, out v) && v is T t)
                    {
                        value = t;
                        return true;
                    }
                }
            }
            value = default;
            return false;
        }

        void IBehaviourTreeExecutable.VisitAllValues(System.Action<string, object> visitor)
        {
            if (visitor == null)
                return;
            lock (mLock)
            {
                foreach(var kv in mBlackboard)
                {
                    visitor(kv.Key, kv.Value);
                }
            }
        }

        #endregion

#if UNITY_EDITOR

        [ContextMenu("Debug AI", true)]
        bool CanDebugAI()
        {
            return mPlayer != null && mPlayer.IsValidInstance();
        }

        [ContextMenu("Debug AI")]
        void DebugAI()
        {
            BehaviourTreeUtils.OpenPlayer(mPlayer, true);
        }

        protected virtual void OnValidate()
        {
            if (mPlayer != null)
                mPlayer.SetBehaviourTree(m_Asset);
        }

#endif
    }
}
